package com.guli.secKill.service.impl;
/*
 * @Author 罗俊
 * @date 2020/11/30 - 1:44 下午
 *
 */

import com.alibaba.fastjson.JSON;
import com.guli.common.constant.CouponConstant;
import com.guli.common.constant.SecKillConstant;
import com.guli.common.pojo.SeckillSkuRelationEntity;
import com.guli.common.pojo.vo.SecKillSessionAndRelationVo;
import com.guli.common.pojo.vo.SkuVo;
import com.guli.common.to.SpuAndSkuListTo;
import com.guli.secKill.feign.CouponFeignClient;
import com.guli.secKill.feign.ProductFeignClient;
import com.guli.secKill.feign.WareFeignClient;
import com.guli.secKill.service.UploadSecKillSkuService;
import com.guli.secKill.vo.SecKillRelationAndSpu;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.ArrayUtils;
import org.redisson.api.RSemaphore;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;

import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Service
@Slf4j
public class UploadSecKillSkuServiceImpl implements UploadSecKillSkuService {

    @Resource
    CouponFeignClient couponFeignClient;

    @Resource
    ProductFeignClient productFeignClient;

    @Resource
    RedisTemplate redisTemplate;
//    StringRedisTemplate redisTemplate;

    @Resource
    RedissonClient redissonClient;

    @Resource
    WareFeignClient wareFeignClient;

    @Override
    public void uploadSkuToRedis(int days) {

        // 1、获取要上架的商品
        List<SecKillSessionAndRelationVo> secKillSessionAndRelationVos = couponFeignClient.getLatestSecKillSkuByDateInterval(days);

        // 2、将上架的场次信息上架到redis
        uploadSessionInfoAndRelationToRedis(secKillSessionAndRelationVos);

        // 3、远程查询商品，将商品也上传redis
        uploadSpuAndSkuToRedisForParseSkuIds(secKillSessionAndRelationVos);

    }

    /**
     * 将上架的场次信息上架到redis
     *
     * @param skuForUpload
     */
    private void uploadSessionInfoAndRelationToRedis(List<SecKillSessionAndRelationVo> skuForUpload) {

        BoundHashOperations<String, String, List<SeckillSkuRelationEntity>> boundHashOperations = redisTemplate.boundHashOps(SecKillConstant.PROMOTION_SESSION_CACHE_PREFIX);

        for (SecKillSessionAndRelationVo vo : skuForUpload) {
            Long startDate = vo.getStartTime().getTime();
            Long endDate = vo.getEndTime().getTime();
            String promotion_session_key = startDate + "_" + endDate;

            if (CollectionUtils.isEmpty(vo.getSeckillSkuRelationEntities())) {
                log.warn("场次id (sessionId) = {}其没有关联秒杀商品，现在不上传该场次信息（保证幂等性）", vo.getId());
                continue;
            }

            Boolean hasKey = redisTemplate.hasKey(promotion_session_key);

            if (!ObjectUtils.isEmpty(hasKey) && hasKey) {
                log.warn("本次秒杀的场次信息之前已经上架到redis过了，现在就不重复上架了，要上架的key = {}", promotion_session_key);
                return;
            }

            boundHashOperations.put(promotion_session_key, vo.getSeckillSkuRelationEntities());
//            opsForList.rightPushAll(promotion_session_key, vo.getSeckillSkuRelationEntities());


            //3、设置该活动的过期时间
            redisTemplate.expire(promotion_session_key, endDate - startDate, TimeUnit.MILLISECONDS);
        }
    }


    private void uploadSpuAndSkuToRedisForParseSkuIds(List<SecKillSessionAndRelationVo> sessionAndRelationVos) {

        List<Long> spuIds = new ArrayList<>();

        /**
         * 解析出所有的skuId作为一个集合，然后一次性去 product-service 查询所需要的商品数据
         */
        sessionAndRelationVos.stream().forEach(sessionAndRelation -> {
            List<Long> skuIdsLocal = sessionAndRelation.getSeckillSkuRelationEntities().stream().map(relation -> {
                return relation.getSpuId();
            }).collect(Collectors.toList());
            spuIds.addAll(skuIdsLocal);
        });

        if (CollectionUtils.isEmpty(spuIds)) {

            log.info("近{}天没有需要提前上架的的秒杀商品，现在结束提前上传", SecKillConstant.PRE_UPLOAD_DAYS);

            return;
        }

        uploadSpuAndSkuToRedis(spuIds, sessionAndRelationVos);

    }

    private void uploadSpuAndSkuToRedis(List<Long> spuIds, List<SecKillSessionAndRelationVo> sessionAndRelationVos) {

        // 根据 spuIdList 查询到所需要的商品数据，结果是个以 spuId 为key；spu以及其sku为value的一个map
        Map<Long, SpuAndSkuListTo> spuAndSkuMap = productFeignClient.getSpuAndSkuBySpuId(spuIds);

        Map<Long, Integer> skuStockNumMap = queryEachSkuStockAndSetStock(spuAndSkuMap.values());
        /**
         * 都已经是通过 hashMap 来存储，就没必要再区分hashMap的名字了
         */
//            String boundHashOpsKey = SecKillConstant.SPU_PREFIX+sessionAndRelationVo.getId();

        // 绑定 redis的 map操作
        BoundHashOperations<String, String, SecKillRelationAndSpu> boundHashOps = redisTemplate.boundHashOps(SecKillConstant.PRODUCT_SPU_PREFIX);

        // 遍历促销场次 session
        for (SecKillSessionAndRelationVo sessionAndRelationVo : sessionAndRelationVos) {


            for (SeckillSkuRelationEntity seckillSkuRelationEntity : sessionAndRelationVo.getSeckillSkuRelationEntities()) {


                /**
                 * 秒杀 spuKey 的key
                 * 如果只有前缀和商品id的话，那么不同场次的相同商品要参与秒杀，那么后面那个场次的会将前面那个场次的替换掉
                 * 所以采用了：前缀+场次id+skuId 来作为key
                 * */
                String spuKey = sessionAndRelationVo.getId() + "_" + seckillSkuRelationEntity.getSpuId();

                Boolean hasSkuKey = boundHashOps.hasKey(spuKey);

                if (!ObjectUtils.isEmpty(hasSkuKey) && hasSkuKey) {
                    log.warn("此次上架的秒杀商品信息，spuKey = {}已经存在，现在跳过，继续上传下一商品 spuKey（保证幂等性）", spuKey);
                    continue;
                }


                // 遍历 促销 relation
                SecKillRelationAndSpu secKillRelationAndSpu = new SecKillRelationAndSpu();

                // 1、设置商品的秒杀信息
                secKillRelationAndSpu.setSeckillSkuRelation(seckillSkuRelationEntity);

                // 2、设置商品的秒杀信息，封装 促销relation 和 spu 与 sku
                secKillRelationAndSpu.setSpuAndSkuListTo(spuAndSkuMap.get(seckillSkuRelationEntity.getSpuId()));

                // 3、设置该商品的秒杀开始时间与结束时间
                secKillRelationAndSpu.setStartTime(sessionAndRelationVo.getStartTime().getTime());

                secKillRelationAndSpu.setEndTime(sessionAndRelationVo.getEndTime().getTime());


//                secKillRelationAndSpu.setRandomCode(randomCode);  randomCode 随机码已经由 整体为 spu设置，现在改为了为每一个spu下的sku设置随机码

                // 信号量、超时时间
                long timeOut = sessionAndRelationVo.getEndTime().getTime() - sessionAndRelationVo.getStartTime().getTime();


                for (SkuVo skuVo : secKillRelationAndSpu.getSpuAndSkuListTo().getSkuVos()) {

                    // 4、设置秒杀的随机码（随机码用于防止秒杀未开始，有人疯狂下单）
                    // /purchase?skuId=12&randomCode=998 只有请求中带有随机码才能参与秒杀
                    // 用于参与秒杀所必须的随机码（要使用此随机码才能获取到该商品的分布式锁、信号量、库存）
                    String randomCode = UUID.randomUUID().toString();

                    skuVo.setRandomCode(randomCode);

                    skuVo.setSkuDesc(null);

                    // 该 sku 的库存（信号量）的key（前缀加上一段随机码作为key）
                    String skuSemaphoreKey = getProductSemaphoreKey(skuVo.getSpuId(), skuVo.getSkuId(), randomCode);

                    // 5、设置该商品的信号量，引入分布式信号量
                    RSemaphore semaphore = redissonClient.getSemaphore(skuSemaphoreKey);
                    // 信号量就是该商品可以秒杀的数量
//                    semaphore.trySetPermits(seckillSkuRelationEntity.getSeckillCount()); // 这样的库存量是写死的
//                    我的库存量采用直接从数据库中查询
                    int skuCount = ObjectUtils.isEmpty(skuStockNumMap.get(skuVo.getSkuId())) ? 0 : skuStockNumMap.get(skuVo.getSkuId());
                    semaphore.trySetPermits(skuCount);

                    // 设置库存信号量的超时时间（设置不上去,干脆直接注释）
//                    redisTemplate.expire(skuSemaphoreKey, timeOut, TimeUnit.MILLISECONDS);
                }


                boundHashOps.put(spuKey, secKillRelationAndSpu);

                // 每一次将新的秒杀商品放入redis的时候，都要刷新存放秒杀商品的map的ttl，
                //TODO 如果 存放秒杀商品的 map一直有新的秒杀商品放入，那么将不能清除老的已经秒杀完成的商品缓存
                redisTemplate.expire(SecKillConstant.PRODUCT_SPU_PREFIX, timeOut, TimeUnit.MILLISECONDS);
            }

        }


    }

    public static String getProductSemaphoreKey(Long spuId, Long skuId, String randomCode) {

        return SecKillConstant.REDIS_SON_SPU_SEMAPHORE_PREFIX + spuId + "_" + skuId + "_" + randomCode;

    }


    private Map<Long, Integer> queryEachSkuStockAndSetStock(Collection<SpuAndSkuListTo> spuAndSkuListTos) {

        List<Long> skuIds = new ArrayList<>();

        for (SpuAndSkuListTo spuAndSkuListTo : spuAndSkuListTos) {
            spuAndSkuListTo.getSkuVos().forEach(skuVo -> {
                skuIds.add(skuVo.getSkuId());
            });
        }

        return wareFeignClient.getStockBySkuIdArray(skuIds);

    }
}
